
#ifndef UTILS_BUFFERS
#define UTILS_BUFFERS

#include "OpenglCore.h"

#include <vector>

#include <memory>

enum class LoadBufferMode {
    StaticDraw, DynamicDraw, StreamDraw
};

enum class HandleBufferMode {
    ReadOnly, WriteOnly, ReadAndWrite
};

enum class BatchBufferMode {
    Interleave, Separate
};

enum class VertexElementType {
    Float, Vec2f, Vec3f, Vec4f,
    Int, Vec2i, Vec3i, Vec4i,
    Mat2f, Mat3f, Mat4f,
    Bool,
};

static uint32_t GetVertexElementSize(const VertexElementType type) {
    switch (type) {
        default: return 0;
        case VertexElementType::Float:		return 4 * 1;
        case VertexElementType::Vec2f:		return 4 * 2;
        case VertexElementType::Vec3f:		return 4 * 3;
        case VertexElementType::Vec4f:		return 4 * 4;
        case VertexElementType::Int:		return 4 * 1;
        case VertexElementType::Vec2i:		return 4 * 2;
        case VertexElementType::Vec3i:		return 4 * 3;
        case VertexElementType::Vec4i:		return 4 * 4;
        case VertexElementType::Mat2f:		return 4 * 2 * 2;
        case VertexElementType::Mat3f:		return 4 * 3 * 3;
        case VertexElementType::Mat4f:		return 4 * 4 * 4;
        case VertexElementType::Bool:		return 1 * 1;
    }
}
static uint32_t GetVertexElementCount(const VertexElementType type) {
    switch (type) {
        default: return 0;
        case VertexElementType::Float:		return 1;
        case VertexElementType::Vec2f:		return 2;
        case VertexElementType::Vec3f:		return 3;
        case VertexElementType::Vec4f:		return 4;
        case VertexElementType::Int:		return 1;
        case VertexElementType::Vec2i:		return 2;
        case VertexElementType::Vec3i:		return 3;
        case VertexElementType::Vec4i:		return 4;
        case VertexElementType::Mat2f:		return 2 * 2;
        case VertexElementType::Mat3f:		return 3 * 3;
        case VertexElementType::Mat4f:		return 4 * 4;
        case VertexElementType::Bool:		return 1;
    }
}

static std::string ToString(const VertexElementType type) {
    switch (type) {
        default: return "";
        case VertexElementType::Float:		return "Float";
        case VertexElementType::Vec2f:		return "Vec2f";
        case VertexElementType::Vec3f:		return "Vec3f";
        case VertexElementType::Vec4f:		return "Vec4f";
        case VertexElementType::Int:		return "Int";
        case VertexElementType::Vec2i:		return "Vec2i";
        case VertexElementType::Vec3i:		return "Vec3i";
        case VertexElementType::Vec4i:		return "Vec4i";
        case VertexElementType::Mat2f:		return "Mat2f";
        case VertexElementType::Mat3f:		return "Mat3f";
        case VertexElementType::Mat4f:		return "Mat4f";
        case VertexElementType::Bool:		return "Bool";
    }
}

static GLenum ToGLenum(const LoadBufferMode loadBufferMode) {
    switch (loadBufferMode) {
        default: return GL_NONE;
        case LoadBufferMode::StaticDraw:	return GL_STATIC_DRAW;
        case LoadBufferMode::DynamicDraw:	return GL_DYNAMIC_DRAW;
        case LoadBufferMode::StreamDraw:	return GL_STREAM_DRAW;
    }
}
static GLenum ToGLenum(const HandleBufferMode handleMode) {
    switch (handleMode) {
        default: return GL_NONE;
        case HandleBufferMode::ReadOnly:		return GL_READ_ONLY;
        case HandleBufferMode::WriteOnly:		return GL_WRITE_ONLY;
        case HandleBufferMode::ReadAndWrite:	return GL_READ_WRITE;
    }
}
static GLenum ToGLenum(const VertexElementType type) {
    switch (type) {
        default: return GL_NONE;
        case VertexElementType::Float:		return GL_FLOAT;
        case VertexElementType::Vec2f:		return GL_FLOAT;
        case VertexElementType::Vec3f:		return GL_FLOAT;
        case VertexElementType::Vec4f:		return GL_FLOAT;
        case VertexElementType::Int:		return GL_INT;
        case VertexElementType::Vec2i:		return GL_INT;
        case VertexElementType::Vec3i:		return GL_INT;
        case VertexElementType::Vec4i:		return GL_INT;
        case VertexElementType::Mat2f:		return GL_FLOAT;
        case VertexElementType::Mat3f:		return GL_FLOAT;
        case VertexElementType::Mat4f:		return GL_FLOAT;
        case VertexElementType::Bool:		return GL_BOOL;
    }
}

struct VertexElement {
    std::string name;
    VertexElementType type;
    uint32_t offset;
    bool normalized = false;

    VertexElement(const std::string& _name, const VertexElementType _type, const bool _normalized)
        : name(_name), type(_type), offset(0), normalized(_normalized) {}
};

class VertexBufferLayout {
public:
    uint32_t stride = 0;
    std::vector<VertexElement> layouts;

    std::vector<VertexElement>::iterator begin() { return layouts.begin(); }
    std::vector<VertexElement>::iterator end() { return layouts.end(); }
    std::vector<VertexElement>::const_iterator begin() const { return layouts.begin(); }
    std::vector<VertexElement>::const_iterator end() const { return layouts.end(); }

    int getComponentsCount() const {
        return layouts.size();
    }

    void setLayouts(const std::initializer_list<VertexElement>& _layouts) { layouts = _layouts; }

    void update(const BatchBufferMode batchBufferMode, const uint32_t allDataSize) {
        switch (batchBufferMode) {
            default: break;
            case BatchBufferMode::Interleave: {
                uint32_t offset = 0;
                for (auto& element : layouts) {
                    element.offset = offset;
                    offset += GetVertexElementSize(element.type);
                }
                stride = offset;
                break;
            }
            case BatchBufferMode::Separate: {
                uint32_t singleVertexSize = 0;
                for (auto& element : layouts)
                    singleVertexSize += GetVertexElementSize(element.type);

                uint32_t vertexNum = allDataSize / singleVertexSize;
                uint32_t offset = 0;
                for (auto& layout : layouts) {
                    layout.offset = offset;
                    offset += vertexNum * GetVertexElementSize(layout.type);
                }
                stride = GetVertexElementSize(layouts[0].type);
                break;
            }
        }
    }
};

class VertexBuffer {
public:
    uint32_t renderID = 0;
    uint32_t attributeDivisor = 0;
    LoadBufferMode loadBufferMode = LoadBufferMode::StaticDraw;
    BatchBufferMode batchBufferMode = BatchBufferMode::Interleave;
    VertexBufferLayout layout;

    ~VertexBuffer() {
        GLCALL(glDeleteBuffers(1, &renderID));
    }

    void setAllBuffer(
        void* buffer,
        const int size,
        const LoadBufferMode _loadBufferMode,
        const BatchBufferMode _batchBufferMode,
        const uint32_t _attributeDivisor
    ) {
        if (0 == layout.getComponentsCount()) {
            throw std::runtime_error("Layout should be set first!");
            return;
        }
		layout.update(batchBufferMode, size);

		loadBufferMode = _loadBufferMode;
		batchBufferMode = _batchBufferMode;
		attributeDivisor = _attributeDivisor;

		GLCALL(glGenBuffers(1, &renderID));
		GLCALL(glBindBuffer(GL_ARRAY_BUFFER, renderID));
		GLCALL(glBufferData(GL_ARRAY_BUFFER, size, buffer, ToGLenum(loadBufferMode)));
    }

    void setLayout(const std::initializer_list<VertexElement>& _layout) {
        layout.setLayouts(_layout);
    }

    void bind() const {
        GLCALL(glBindBuffer(GL_ARRAY_BUFFER, renderID));
    }
    void unbind() const {
        GLCALL(glBindBuffer(GL_ARRAY_BUFFER, 0));
    }
};


class IndexBuffer {
public:
    uint32_t renderID = 0;
    uint32_t count = 0;
    LoadBufferMode loadBufferMode = LoadBufferMode::StaticDraw;

    ~IndexBuffer() {
        GLCALL(glDeleteBuffers(1, &renderID));
    }

    void setAllBuffer(void* buffer, const uint32_t _count, const LoadBufferMode _loadBufferMode) {
        if (0 == _count) {
            throw std::runtime_error("Count should not be zero!");
            return;
        }
		count = _count;
        loadBufferMode = _loadBufferMode;

		GLCALL(glGenBuffers(1, &renderID));
		GLCALL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, renderID));
		GLCALL(glBufferData(GL_ELEMENT_ARRAY_BUFFER, count * sizeof(uint32_t), buffer, ToGLenum(_loadBufferMode)));
    }

    void bind() const {
        GLCALL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, renderID));
    }
    void unbind() const {
        GLCALL(glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0));
    }
};

class VertexArray {
public:
    uint32_t renderID = 0;
    uint32_t index = 0;
    std::vector< std::shared_ptr<VertexBuffer> > vbos;
    std::shared_ptr<IndexBuffer> ibo;

    VertexArray() {
        GLCALL(glGenVertexArrays(1, &renderID));
        GLCALL(glBindVertexArray(renderID));
    }

    ~VertexArray() {
        GLCALL(glDeleteVertexArrays(1, &renderID));
    }

    void addVertexBuffer(const std::shared_ptr<VertexBuffer>& vbo) {
		vbos.push_back(vbo);

		for (auto& component : vbo->layout) {
			GLCALL(glEnableVertexAttribArray(index));
			GLCALL(glVertexAttribPointer(
                    index,
				    GetVertexElementCount(component.type),
				    ToGLenum(component.type),
				    component.normalized ? GL_TRUE : GL_FALSE,
				    vbo->layout.stride,
				    reinterpret_cast<void*>(component.offset)
                ));
			GLCALL(glVertexAttribDivisor(index, vbo->attributeDivisor));
			++index;
		}
    }

    void setIndexBuffer(const std::shared_ptr<IndexBuffer>& _ibo) {
        ibo = _ibo;
    }

    void bind() const {
        GLCALL(glBindVertexArray(renderID));
		for (const auto& vbo : vbos)
			vbo->bind();
		ibo->bind();
    }
    void unbind() const {
        GLCALL(glBindVertexArray(0));
		for (const auto& vbo : vbos)
			vbo->unbind();
		ibo->unbind();
    }
};

class ShadowMapBuffer {
public:
    uint32_t fbo_renderID = 0;
    uint32_t cbo_renderID = 0;
    int size = 0;

	~ShadowMapBuffer() {
		GLCALL(glDeleteTextures(1, &cbo_renderID));
		GLCALL(glDeleteFramebuffers(1, &fbo_renderID));
		cbo_renderID = 0;
		fbo_renderID = 0;
	}

    void setAllBuffer(const int _size) {
		if (0 == _size || size == _size)
			return;

		if (0 != fbo_renderID) {
			GLCALL(glDeleteTextures(1, &cbo_renderID));
			GLCALL(glDeleteFramebuffers(1, &fbo_renderID));
		}

        size = _size;

		/* Frame buffer */
		GLCALL(glGenFramebuffers(1, &fbo_renderID));
		GLCALL(glBindFramebuffer(GL_FRAMEBUFFER, fbo_renderID));

		/* Color buffer */
        GLCALL(glGenTextures(1, &cbo_renderID));
        GLCALL(glBindTexture(GL_TEXTURE_2D, cbo_renderID));

		GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
		GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
		GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
		GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));

        GLCALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_DEPTH_COMPONENT, _size, _size, 0, GL_DEPTH_COMPONENT, GL_FLOAT, NULL));

        GLCALL(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_DEPTH_ATTACHMENT, GL_TEXTURE_2D, cbo_renderID, 0));
        GLCALL(glDrawBuffer(GL_NONE));
        GLCALL(glReadBuffer(GL_NONE));

		/* Check frame buffer */
		GLCALL(auto success = glCheckFramebufferStatus(GL_FRAMEBUFFER));
        if (!success) {
            throw std::runtime_error("Frame buffer is not complete!");
            return;
        }

		/* Unbind all */
		GLCALL(glBindFramebuffer(GL_FRAMEBUFFER, 0));
		GLCALL(glBindTexture(GL_TEXTURE_2D, 0));
    }

	void bindAttachments(const uint32_t slot) const {
		/* Color buffer */
		GLCALL(glActiveTexture(GL_TEXTURE0 + slot));
		GLCALL(glBindTexture(GL_TEXTURE_2D, cbo_renderID));
	}
	void unbindAttachments() const {
		/* Color buffer */
		GLCALL(glBindTexture(GL_TEXTURE_2D, 0));
	}

	void bind() const {
		GLCALL(glBindFramebuffer(GL_FRAMEBUFFER, fbo_renderID));
	}
	void unbind() const {
		GLCALL(glBindFramebuffer(GL_FRAMEBUFFER, 0));
	}
};

class FrameBuffer {
public:
    uint32_t fbo_renderID = 0;
    uint32_t cbo_renderID = 0;
    uint32_t dsb_renderID = 0;
    int width = 0;
    int height = 0;

	~FrameBuffer() {
		GLCALL(glDeleteTextures(1, &cbo_renderID));
		GLCALL(glDeleteRenderbuffers(1, &dsb_renderID));
		GLCALL(glDeleteFramebuffers(1, &fbo_renderID));
		cbo_renderID = 0;
		dsb_renderID = 0;
		fbo_renderID = 0;
	}

	void setAllBuffer(const int _width, const int _height) {
		if ((0 == _width || 0 == _height) || (width == _width && height == _height))
			return;

		if (0 != fbo_renderID) {
			GLCALL(glDeleteTextures(1, &cbo_renderID));
			GLCALL(glDeleteRenderbuffers(1, &dsb_renderID));
			GLCALL(glDeleteFramebuffers(1, &fbo_renderID));
		}

        width = _width;
        height = _height;

		/* Frame buffer */
		GLCALL(glGenFramebuffers(1, &fbo_renderID));
		GLCALL(glBindFramebuffer(GL_FRAMEBUFFER, fbo_renderID));

		/* Color buffer */
		GLCALL(glGenTextures(1, &cbo_renderID));
		GLCALL(glBindTexture(GL_TEXTURE_2D, cbo_renderID));

		GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR));
		GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR));
		GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE));
		GLCALL(glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE));

		GLCALL(glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB8, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, nullptr));

		GLCALL(glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, cbo_renderID, 0));

		/* Depth and stencil buffer */
		GLCALL(glGenRenderbuffers(1, &dsb_renderID));
		GLCALL(glBindRenderbuffer(GL_RENDERBUFFER, dsb_renderID));

		GLCALL(glRenderbufferStorage(GL_RENDERBUFFER, GL_DEPTH24_STENCIL8, width, height));

		GLCALL(glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_DEPTH_STENCIL_ATTACHMENT, GL_RENDERBUFFER, dsb_renderID));

		/* Check frame buffer */
		GLCALL(auto success = glCheckFramebufferStatus(GL_FRAMEBUFFER));
        if (!success) {
            throw std::runtime_error("Frame buffer is not complete!");
            return;
        }

		/* Unbind all */
		GLCALL(glBindTexture(GL_TEXTURE_2D, 0));
		GLCALL(glBindRenderbuffer(GL_RENDERBUFFER, 0));
		GLCALL(glBindFramebuffer(GL_FRAMEBUFFER, 0));
	}

	void bindAttachments(const uint32_t slot) const {
		/* Color buffer */
		GLCALL(glActiveTexture(GL_TEXTURE0 + slot));
		GLCALL(glBindTexture(GL_TEXTURE_2D, cbo_renderID));

		/* Depth and stencil buffer */
		GLCALL(glBindRenderbuffer(GL_RENDERBUFFER, dsb_renderID));
	}
	void unbindAttachments() const {
		/* Color buffer */
		GLCALL(glBindTexture(GL_TEXTURE_2D, 0));

		/* Depth and stencil buffer */
		GLCALL(glBindRenderbuffer(GL_RENDERBUFFER, 0));
	}

	void bind() const {
		GLCALL(glBindFramebuffer(GL_FRAMEBUFFER, fbo_renderID));
	}
	void unbind() const {
		GLCALL(glBindFramebuffer(GL_FRAMEBUFFER, 0));
	}
};

#endif // UTILS_BUFFERS
